package com.apples.hexviewer2.utils.io;

import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.UriPermission;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Log;

import com.apples.hexviewer2.utils.SysHelper;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;

/**
 * ******************************************************************************
 * <p><b>Project HexViewer</b><br/>
 * File functions.
 * </p>
 *
 * @author Keidan
 * <p>
 * License: GPLv3
 * </p>
 * ******************************************************************************
 */
public class FileHelper {
    private static final String FILE_HELPER_TAG = "FileHelper";
    private static final String EXCEPTION_TAG = "Exception: ";
    private static final String BUCKET_ID = "?bucketId=";

    private FileHelper() {
    }

    /**
     * Prepares the intent for the directory opening mode.
     *
     * @return Intent
     */
    public static Intent prepareForOpenDirectory() {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        return intent;
    }

    /**
     * Prepares the intent for the file opening mode.
     *
     * @return Intent
     */
    public static Intent prepareForOpenFile(final Context c) {
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(c);
        String filePath = preferences.getString("hex_viewer_open_path", null);
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.setType("*/*");
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        if (!TextUtils.isEmpty(filePath)) {
            File root = Environment.getExternalStorageDirectory();
            filePath = filePath.replaceFirst(root.getAbsolutePath(), "")
                    .replaceFirst("/", "%3A")
                    .replace("/", "%2F");
            Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary" + filePath);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri,
                        DocumentsContract.getTreeDocumentId(uri));
                intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, docUri);
            } else {
                intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri);
            }
        }
        return intent;
    }

    /**
     * Retrieving the permissions associated with a Uri
     *
     * @param c       Android content.
     * @param uri     Uri
     * @param fromDir From dir ?
     * @return False if permission is not granted for this Uri.
     */
    public static boolean takeUriPermissions(final Context c, final Uri uri, boolean fromDir) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q)
            return true;
        boolean success = false;
        try {
            final int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
            c.getContentResolver().takePersistableUriPermission(uri, takeFlags);
            if (!fromDir) {
                takUriPermissionsForDir(c, uri);
            }
            success = true;
        } catch (Exception e) {
            Log.e(SysHelper.class.getSimpleName(), EXCEPTION_TAG + e.getMessage(), e);
        }
        return success;
    }

    private static void takUriPermissionsForDir(final Context c, final Uri uri) {
        Uri dir = getParentUri(c, uri);
        if (!hasUriPermission(c, dir, false))
            try {
                c.getContentResolver().takePersistableUriPermission(dir, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            } catch (Exception e) {
                Log.e(SysHelper.class.getSimpleName(), EXCEPTION_TAG + e.getMessage(), e);
            }
    }

    /**
     * Releases the permissions associated to a Uri.
     *
     * @param c   Android content.
     * @param uri Uri
     */
    public static void releaseUriPermissions(final Context c, final Uri uri) {
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q && hasUriPermission(c, uri, true))
            try {
                final int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
                c.getContentResolver().releasePersistableUriPermission(uri, takeFlags);

                Uri dir = getParentUri(c, uri);
                final List<UriPermission> list = c.getContentResolver().getPersistedUriPermissions();
                int found = 0;
                for (UriPermission up : list) {
                    if (up.getUri().equals(dir)) {
                        found++;
                    }
                }
                if (found == 1) {
                    c.getContentResolver().releasePersistableUriPermission(dir, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                }
            } catch (Exception e) {
                Log.e(SysHelper.class.getSimpleName(), EXCEPTION_TAG + e.getMessage(), e);
            }
    }

    /**
     * Test if permission is granted for this Uri.
     *
     * @param c              Android content.
     * @param uri            Uri
     * @param readPermission True = read, false = write.
     * @return False if permission is not granted for this Uri.
     */
    public static boolean hasUriPermission(final Context c, final Uri uri, boolean readPermission) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q)
            return true;
        final List<UriPermission> list = c.getContentResolver().getPersistedUriPermissions();
        boolean found = false;
        for (UriPermission up : list) {
            if (up.getUri().equals(uri) && ((up.isReadPermission() && readPermission) || (up.isWritePermission() && !readPermission))) {
                found = true;
                break;
            }
        }
        return found;
    }

    /**
     * Test if the file pointed by the Uri exists or not.
     *
     * @param ctx Android context.
     * @param cr  ContentResolver
     * @param uri Uri
     * @return boolean
     */
    public static boolean isFileExists(Context ctx, final ContentResolver cr, final Uri uri) {
        ParcelFileDescriptor pfd;
        boolean exists = false;
        try {
            pfd = cr.openFileDescriptor(uri, "r");
            pfd.close();
            exists = true;
        } catch (Exception e) {
            Log.e(SysHelper.class.getSimpleName(), EXCEPTION_TAG + e.getMessage(), e);
        }
        return exists;
    }

    /**
     * Returns the size of the file pointed to by the Uri.
     *
     * @param ctx Android context.
     * @param cr  ContentResolver
     * @param uri Uri
     * @return long (-1 = FileNotFoundException, -2 = other errors)
     */
    public static long getFileSize(Context ctx, ContentResolver cr, Uri uri) {
        ParcelFileDescriptor pfd = null;
        long size;
        try {
            pfd = cr.openFileDescriptor(uri, "r");
            long sz = pfd.getStatSize();
            pfd.close();
            size = sz;
        } catch (Exception e) {
            Log.e(SysHelper.class.getSimpleName(), EXCEPTION_TAG + e.getMessage()/*, e*/);
            size = e instanceof FileNotFoundException ? -1 : -2;
        } finally {
            if (pfd != null)
                try {
                    pfd.close();
                } catch (IOException e) {
                    Log.e(SysHelper.class.getSimpleName(), EXCEPTION_TAG + e.getMessage()/*, e*/);
                }
        }
        return size;
    }

    /**
     * Gets the file name from a Uri.
     *
     * @param ctx Android context.
     * @param uri Uri
     * @return String
     */
    public static String getFileName(final Context ctx, final Uri uri) {
        String result = null;
        if (uri.getScheme().equals("content")) {
            try (Cursor cursor = ctx.getContentResolver().query(uri, null, null, null, null)) {
                if (cursor != null && cursor.moveToFirst()) {
                    int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
                    if (index >= 0) {
                        result = cursor.getString(index);
                    }
                }
            } catch (Exception e) {
                Log.e(FileHelper.class.getSimpleName(), EXCEPTION_TAG + e.getMessage()/*, e*/);
            }
        }
        if (result == null) {
            result = extractName(getPathFromBucket(ctx, uri));
        }

        if (result == null) {
            result = extractName(uri.getPath());
        }
        return result;
    }

    public static Uri adjustUri(final Context ctx, final Uri uri) {
        String path = getPathFromBucket(ctx, uri);
        if (path == null)
            return uri;
        return Uri.fromFile(new File(path));
    }

    /**
     * File name extraction.
     *
     * @param name Full name.
     * @return File name.
     */
    private static String extractName(final String name) {
        String result = name;
        if (result != null) {
            int cut = result.lastIndexOf('/');
            if (cut != -1) {
                result = result.substring(cut + 1);
            }
        }
        return result;
    }

    /**
     * Extracting the path from a URI with bucket id.
     *
     * @param ctx Android context.
     * @param uri Uri
     * @return String
     */
    private static String getPathFromBucket(final Context ctx, final Uri uri) {
        String result = null;
        int bucketId = uri.toString().indexOf(BUCKET_ID);
        if (bucketId != -1) {
            String bid = uri.toString().substring(bucketId + BUCKET_ID.length());
            final String[] projectionBucket = {
                    MediaStore.MediaColumns.BUCKET_ID,
                    MediaStore.MediaColumns.DATA};
            final String groupBy = "1) GROUP BY 1,(2";
            final String orderBy = "MAX(" + MediaStore.MediaColumns.DATE_TAKEN + ") DESC";
            try (Cursor cursor = ctx.getContentResolver().query(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projectionBucket, groupBy, null, orderBy)) {
                if (cursor != null && cursor.moveToFirst()) {
                    int dataColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
                    int idColumn = cursor.getColumnIndex(MediaStore.MediaColumns.BUCKET_ID);
                    do {
                        String data = cursor.getString(dataColumn);
                        String id = cursor.getString(idColumn);
                        if (id.equals(bid)) {
                            result = data;
                            break;
                        }
                    } while (cursor.moveToNext());
                }
            }
        }
        return result;
    }

    /**
     * Gets the parent from a Uri.
     *
     * @param uri Uri
     * @return String
     */
    public static Uri getParentUri(final Context ctx, final Uri uri) {
        final String filename = getFileName(ctx, uri);
        final String encoded = uri.getEncodedPath();
        final int filenameLen = (filename == null ? 0 : filename.length());
        final int length = encoded == null ? 0 : encoded.length() - filenameLen;
        String path;
        if (length > 0 && length < encoded.length()) {
            String parent = encoded.substring(0, encoded.length() - filenameLen);
            if (parent.endsWith("%2F")) {
                parent = parent.substring(0, parent.length() - 3);
            }
            final String documentPrimary = "/document/primary%3A";
            if (parent.startsWith(documentPrimary)) {
                path = "/tree/primary%3A" + parent.substring(documentPrimary.length());
            } else {
                path = parent;
            }
        } else {
            path = encoded;
        }
        return Uri.parse(uri.getScheme() + "://" + uri.getHost() + path);
    }
}
